# -*- coding: utf-8 -*-
#
# Copyright (c) 2012, Rafael Pivato
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#     * Redistributions of source code must retain the above copyright
#       notice, this list of conditions and the following disclaimer.
#     * Redistributions in binary form must reproduce the above copyright
#       notice, this list of conditions and the following disclaimer in the
#       documentation and/or other materials provided with the distribution.
#     * Neither the name of Rafael Pivato nor the names of its contributors
#       may be used to endorse or promote products derived from this
#       software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL RAFAEL PIVATO BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

import random
import pickle

from datetime import datetime
from google.appengine.ext import db
from wowgae.model.wiki import WikiPage


old_symbols = 'bdefghjkpqrstuvwxyzACDEFGHJKLMNOPRSTUVWXYZ23456789'
alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-'
pennybet = 'bdefghijkmnpqrstuvwxyzACDEFGHJKLMNOPRSTUVWXYZ123456789'
p = 'bdefghjkpqrstuvwxyzACDEFGHJKLMNOPRSTUVWXYZ23456789'


class ExcessiveCollisionsError(Exception):
    """Throw when there were too many collisions trying to generate a code."""

    def __init__(self):
        Exception.__init__('Could not find a new code.')
        self.message = 'Could not find a new code.'


class RandomState(db.Model):
    """Holds random number generator state."""

    seed = db.DateTimeProperty()
    pickled_state = db.BlobProperty()

    def get_state(self):
        return pickle.loads(self.pickled_state)

    def set_state(self, state):
        self.pickled_state = pickle.dumps(state)

    @classmethod
    def random(cls, scope, start, stop):
        """Gets a new random number from the given scope."""
        key_name = scope.lower()

        def txn():
            entity = cls.get_by_key_name(key_name)
            if entity is None:
                seed = datetime.utcnow()
                generator = random.Random(seed)
                entity = cls(key_name=scope.lower(), seed=seed)
            else:
                generator = random.Random(entity.seed)
                generator.setstate(entity.get_state())
            result = generator.randrange(start, stop)
            entity.set_state(generator.getstate())
            entity.put()
            return result
        return db.run_in_transaction(txn)


class ShortURL(db.Model):
    """Holds short URL information."""

    account = db.StringProperty()
    code_a = db.StringProperty()
    code_b = db.StringProperty()
    fancy = db.StringProperty(indexed=False)
    created = db.DateTimeProperty()
    reason = db.StringProperty(choices=['system', 'admin', 'share'])
    url = db.URLProperty()
    used = db.DateTimeProperty()
    clicks = db.IntegerProperty(default=0)
    collisions = db.IntegerProperty(default=0)

    symbols = 'bcdefghijkmnpqrstuvwxyz023456789'

    @classmethod
    def normalize_code(cls, code, remove_hyphens=True):
        code = WikiPage.key_from_name(code)
        if remove_hyphens:
            code = code.replace(u'-', u'')
        return code

    @classmethod
    def to_base10(cls, code):
        code = cls.normalize_code(code)
        base = len(cls.symbols)
        value = 0
        for pos in range(len(code)):
            digit = code[-pos - 1]
            pos_value = (base ** pos)
            digit_value = pos_value * cls.symbols.index(digit)
            value = value + digit_value
        return value

    @classmethod
    def from_base10(cls, num):
        num = int(num)
        base = len(cls.symbols)
        code = []
        while True:
            mod = num % base
            num = num / base
            code.insert(0, cls.symbols[mod])
            if num == 0:
                break
        return ''.join(code)

    @classmethod
    def generate_code(cls, size):
        base = len(cls.symbols)
        stop = (base ** size) - 1
        decimal = RandomState.random('short_url', 0, stop)
        code = cls.from_base10(decimal)
        return code

    @classmethod
    def store_url(cls, reason, user_id, url):

        def store_without_collision(code):
            entity = cls.get_by_key_name(code)
            if entity is None:
                code_a = code[0:3]
                code_b = code[3:5]
                fancy = code_a + '-' + code_b
                now = datetime.utcnow()
                entity = cls(key_name=code, account=user_id, code_a=code_a,
                             code_b=code_b, created=now, reason=reason,
                             url=url, used=now, fancy=fancy)
            else:
                entity.collision += 1
            entity.put()
            return entity

        tries = 5
        code = cls.generate_code(5)
        entity = db.run_in_transaction(store_without_collision, code)
        while entity is None:
            tries = tries - 1
            if tries < 0:
                raise ExcessiveCollisionsError()
            code = cls.generate_code(5)
            entity = db.run_in_transaction(store_without_collision, code)
        return str(entity.fancy)

    @classmethod
    def store_custom_url(cls, reason, user_id, url, code):
        fancy = cls.normalize_code(code, remove_hyphens=False)
        code = fancy.replace('-', '')
        size = len(code)
        mid = size / 2
        code_a = code[0:-mid]
        code_b = code[-mid:size]
        now = datetime.utcnow()
        entity = cls(key_name=code, account=user_id, code_a=code_a,
                     code_b=code_b, created=now, reason=reason,
                     url=url, used=now, fancy=fancy)
        entity.put()
        return fancy

    @classmethod
    def click_code(cls, code):

        def click_and_count(normal_code):
            entity = cls.get_by_key_name(normal_code)
            if entity is None:
                return None
            else:
                entity.clicks += 1
                entity.used = datetime.utcnow()
                entity.put()
                return str(entity.url)

        normal = cls.normalize_code(code)
        return db.run_in_transaction(click_and_count, normal)
